#pragma once

#include <cmath>
#include <numeric>
#include <vector>
#include <algorithm>
#include <array>

#include "base.h"
#include "../impl/minmax.h"
#include "../updater/ma.h"
#include "../updater/s2.h"
#include "../container/robuf.h"
#include "../algo/snqn.h"

namespace mdtx::online::indicator
{
    // not robust

    template <typename T>
    class mae : public indi_unary2<T>
    {
        using base = indi_unary2<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        boost::circular_buffer<T> buf;
        impl::sma<T> m{};

    public:
        explicit mae(ptrdiff_t period)
            : buf(period)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            T x = std::get<0>(in);
            if (buf.full())
            {
                m.roll_impl(x, buf.front());
            }
            else
            {
                m.insert_impl(x);
            }
            buf.push_back(x);

            T e{};
            for (auto elem : buf)
            {
                e += std::abs(elem - m.m);
            }
            return tuple_out(m.m, e);
        }
    };

    template <typename T>
    class stddev : public indi_unary2<T>
    {
        using base = indi_unary2<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        boost::circular_buffer<T> buf;
        updater::var<T> var;

    public:
        explicit stddev(ptrdiff_t period, ptrdiff_t ddof = 1)
            : buf(period),
              var(ddof)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            T x = std::get<0>(in);
            if (buf.full())
            {
                var.remove_impl(buf.front());
            }
            var.insert_impl(x);
            buf.push_back(x);

            auto [m, s] = var.get();
            return tuple_out(m, std::sqrt(s));
        }
    };

    template <typename T>
    class stddev_cu : public indi_unary2<T>
    {
        using base = indi_unary2<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        updater::var<T> var;

    public:
        explicit stddev_cu(ptrdiff_t ddof = 1)
            : var(ddof)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            var.insert(in);
            auto [m, s] = var.get();
            return tuple_out(m, std::sqrt(s));
        }
    };

    template <typename T>
    class stddev_ew : public indi_unary2<T>
    {
        using base = indi_unary2<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        updater::var_ew<T> var;

    public:
        explicit stddev_ew(ptrdiff_t period)
            : var(period)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            var.insert(in);
            auto [m, s] = var.get();
            return tuple_out(m, std::sqrt(s));
        }
    };

    template <typename T>
    class stddev_evw : public indi_weighted2<T>
    {
        using base = indi_weighted2<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        boost::circular_buffer<T> buf;
        impl::sum<T> wsum{};
        impl::var_evw<T> var{};

    public:
        explicit stddev_evw(ptrdiff_t period)
            : buf(period)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            auto [p, v] = in;
            if (buf.full())
            {
                wsum.roll_impl(v, buf.front());
            }
            else
            {
                wsum.insert_impl(v);
            }
            buf.push_back(v);

            auto w = v / wsum.s;
            var.insert_impl(p, w);

            T sd{};
            if (var.s2 > 0)
            {
                sd = std::sqrt(var.s2);
            }
            return tuple_out(var.m, sd);
        }
    };

    template <typename T>
    class stddev_vw : public indi_weighted2<T>
    {
        using base = indi_weighted2<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        boost::circular_buffer<T> buf;
        updater::var_vw<T> var{};

    public:
        explicit stddev_vw(ptrdiff_t period)
            : buf(period * 2)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            if (buf.full())
            {
                var.remove_impl(buf[0], buf[1]);
            }
            var.insert(in);
            bpush(buf, std::get<0>(in), std::get<1>(in));

            auto [m, s] = var.get();
            return tuple_out(m, std::sqrt(s));
        }
    };

    template <typename T>
    class stddev_cuvw : public indi_weighted2<T>
    {
        using base = indi_weighted2<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        updater::var_vw<T> var;

    public:
        tuple_out update(tuple_in const &in) override
        {
            var.insert(in);
            auto [m, s] = var.get();
            return tuple_out(m, std::sqrt(s));
        }
    };

    template <typename T>
    class zledev : public indi_unary2<T>
    {
        using base = indi_unary2<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        updater::var_ew<T> var;
        boost::circular_buffer<T> buf;

    public:
        explicit zledev(ptrdiff_t period)
            : var(period),
              buf(period / 2)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            T x = std::get<0>(in), datum;
            if (buf.full())
            {
                datum = x + (x - buf.front());
            }
            else
            {
                datum = x;
            }
            buf.push_back(x);
            var.insert_impl(datum);

            auto [m, s] = var.get();
            return tuple_out(m, std::sqrt(s));
        }
    };

    // robust

    template <typename T>
    class mad : public indi_unary2<T>
    {
        using base = indi_unary2<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        container::sorted_robuf<T> buf;
        std::vector<T> delta;
        T c;

    public:
        explicit mad(ptrdiff_t period, T constant = 1.4826)
            : buf(period), delta(period), c(constant)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            buf.insert(std::get<0>(in));

            size_t n = buf.buf.size();
            size_t midx = n / 2;
            T m = buf.sorted[midx];
            std::transform(buf.buf.begin(), buf.buf.end(),
                           delta.begin(),
                           [m](const auto &x)
                           {
                               return std::abs(x - m);
                           });
            std::nth_element(delta.begin(), delta.begin() + midx, delta.begin() + n);

            return tuple_out(m, c * delta[midx]);
        }
    };

    template <typename T>
    class iqr : public indi_unary2<T>
    {
        using base = indi_unary2<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        container::quantile_robuf<T, 3> buf;

    public:
        explicit iqr(ptrdiff_t period, T q1 = 0.25, T q3 = 0.75)
            : buf(period, std::array<T, 3>{q1, 0.5, q3})
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            buf.insert(std::get<0>(in));

            auto [q1, q2, q3] = buf.get();
            return tuple_out(q2, q3 - q1);
        }
    };

    template <typename T>
    class Sn : public indi_unary2<T>
    {
        using base = indi_unary2<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        container::sorted_robuf<T> buf;
        algo::sn<T> solver;
        T a2m{}, c{};
        ptrdiff_t midx;

    public:
        explicit Sn(ptrdiff_t period, T constant = 1.1926)
            : buf(period),
              solver(period),
              midx(period / 2)
        {
            c = solver.get_cn(period) * constant;
        }

        tuple_out update(tuple_in const &in) override
        {
            buf.insert(std::get<0>(in));

            T med;
            if (buf.buf.full())
            {
                a2m = solver.run(buf.sorted.cbegin());
                med = buf.sorted[midx];
            }
            else
            {
                med = buf.sorted[buf.buf.size() / 2];
            }

            return tuple_out(med, c * a2m);
        }
    };

    template <typename T>
    class Qn : public indi_unary2<T>
    {
        using base = indi_unary2<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        container::sorted_robuf<T> buf;
        algo::qn<T> solver;
        T q{}, c{};
        ptrdiff_t midx;

    public:
        explicit Qn(ptrdiff_t period, T constant = 2.21914)
            : buf(period),
              solver(period),
              midx(period / 2)
        {
            c = solver.get_dn(period) * constant;
        }

        tuple_out update(tuple_in const &in) override
        {
            buf.insert(std::get<0>(in));

            T med;
            if (buf.buf.full())
            {
                q = solver.run(buf.sorted.cbegin());
                med = buf.sorted[midx];
            }
            else
            {
                med = buf.sorted[buf.buf.size() / 2];
            }

            return tuple_out(med, c * q);
        }
    };

    // arbitrary

    /**
     * @brief Price channel
     *
     * @details centre = SMA((HH + LL) / 2), scale = SMA((HH - LL) / 2), HH = HIGH(high), LL = LOW(low)
     *
     * @tparam T
     */
    template <typename T>
    class pchan : public indi_binary2<T>
    {
        using base = indi_binary2<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        boost::circular_buffer<T> buf;
        impl::minmax<T> mm{};
        impl::sma<T> ma_hl{}, ma_sc{};

    public:
        explicit pchan(ptrdiff_t period)
            : buf(period * 4)
        {
        }

        /**
         * @brief update
         *
         * @param in high, low
         * @return tuple_out
         */
        tuple_out update(tuple_in const &in) override
        {
            auto [high, low] = in;

            if (buf.full())
            {
                mm.remove_impl(buf[0], buf[1]);
            }
            mm.insert_impl(high, low);

            auto hh = mm.maxdeq.front();
            auto ll = mm.mindeq.front();
            auto hl = (hh + ll) / 2;
            auto sc = (hh - ll) / 2;
            if (buf.full())
            {
                ma_hl.roll_impl(hl, buf[2]);
                ma_sc.roll_impl(sc, buf[3]);
            }
            else
            {
                ma_hl.insert_impl(hl);
                ma_sc.insert_impl(sc);
            }

            bpush(buf, high, low, hl, sc);

            return tuple_out(ma_hl.m, ma_sc.m);
        }
    };
}
